﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace Elderos.Clustering
{
    public class ARTClusterizator<T>
    {
        private readonly double _delta;

        private readonly Func<T,T,double> _objectDistanceFunction;
        private readonly Func<T, Cluster<T>, double> _clusterDistanceFunction;
        private readonly Func<IEnumerable<T>, T> _getCenterFunc;

        private int _maxIterationsCount;

        public ARTClusterizator(Func<T,T,double> objectDistanceFunction,
			Func<T, Cluster<T>, double> clusterDistanceFunction, Func<IEnumerable<T>, T> getCenterFunction,
            double delta, int maxIterationsCount = 10)
        {
            _objectDistanceFunction = objectDistanceFunction;
            _clusterDistanceFunction = clusterDistanceFunction;
            _getCenterFunc = getCenterFunction;
            _delta = delta;
            _maxIterationsCount = maxIterationsCount;
        }

        public List<Cluster<T>> Process(IEnumerable<T> objects)
        {
            T[] objlist = objects.ToArray();

            if (objlist.Length <= 0)
				return new List<Cluster<T>>();

            var random = new Random();
            var @new = new List<Cluster<T>>
                           {
                               new Cluster<T>(_getCenterFunc)
                                   {new ClusterElement<T>(objlist[random.Next() % objlist.Length])}
                           };

            for (int iteration = 0; iteration < _maxIterationsCount; iteration++)
            {
                IEnumerable<T> oldCentroids = @new.Select(x => x.Centroid);
                @new = InitializeCentroids(@new);

                IterateObjects(@new, objlist);

                if (AreDifferentClusters(oldCentroids, @new.Select(x => x.Centroid)))
                    break;
            }

            RemoveFakeElements(@new);

            return @new;
        }

        private static void RemoveFakeElements(IEnumerable<Cluster<T>> @new)
        {
            foreach (var cluster in @new)
                cluster.RemoveAll(x => x.IsFake);
        }

        private void IterateObjects(List<Cluster<T>> clusters, IEnumerable<T> objlist)
        {
            foreach (var obj in objlist)
            {
                Cluster<T> nearestCluster = null;
                double? minDistance = null;

                foreach (Cluster<T> cluster in clusters)
                {
                    double distance = _clusterDistanceFunction(obj, cluster);
                    if (minDistance.HasValue && distance >= minDistance) continue;
                    nearestCluster = cluster;
                    minDistance = distance;
                }

                if (minDistance > _delta || nearestCluster == null)
                {
                    nearestCluster = CreateNewCluster(clusters);
                }
                else if (nearestCluster.First().IsFake)
                    nearestCluster.Clear();
                
                nearestCluster.Add(new ClusterElement<T>(obj));
            }
        }

        private List<Cluster<T>> InitializeCentroids(IEnumerable<Cluster<T>> oldClusters)
        {
            var clusters = new List<Cluster<T>>();
            clusters.AddRange(
                oldClusters.Select(x => new Cluster<T>(_getCenterFunc) {new ClusterElement<T>(x.Centroid, true)}));
            return clusters;
        }

        private Cluster<T> CreateNewCluster(List<Cluster<T>> currentClusters)
        {
            var cluster = new Cluster<T>(_getCenterFunc);
            currentClusters.Add(cluster);
            return cluster;
        }

        private bool AreDifferentClusters(IEnumerable<T> old, IEnumerable<T> @new)
        {
            int oldCount = old.Count();
            int newCount = @new.Count();

            if (oldCount == 0 || newCount == 0)
                throw new ArgumentException();

            bool result = oldCount*1.0/newCount > _delta;
            if (result) return true;
            return !@new.All(x => old.Any(y => _objectDistanceFunction(x, y) < _delta));
        }
    }
}
